"""Helper to check the configuration file."""

from __future__ import annotations

from collections import OrderedDict
import logging
import os
from pathlib import Path
from typing import NamedTuple, Self

from annotatedyaml import loader as yaml_loader
import voluptuous as vol

from homeassistant import loader
from homeassistant.config import (  # type: ignore[attr-defined]
    CONF_PACKAGES,
    YAML_CONFIG_FILE,
    config_per_platform,
    extract_domain_configs,
    format_homeassistant_error,
    format_schema_error,
    load_yaml_config_file,
    merge_packages_config,
)
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.core_config import CORE_CONFIG_SCHEMA
from homeassistant.exceptions import HomeAssistantError
from homeassistant.requirements import (
    RequirementsNotFound,
    async_clear_install_history,
    async_get_integration_with_requirements,
)

from . import config_validation as cv
from .typing import ConfigType


class CheckConfigError(NamedTuple):
    """Configuration check error."""

    message: str
    domain: str | None
    config: ConfigType | None


class HomeAssistantConfig(OrderedDict):
    """Configuration result with errors attribute."""

    def __init__(self) -> None:
        """Initialize HA config."""
        super().__init__()
        self.errors: list[CheckConfigError] = []
        self.warnings: list[CheckConfigError] = []

    def add_error(
        self,
        message: str,
        domain: str | None = None,
        config: ConfigType | None = None,
    ) -> Self:
        """Add an error."""
        self.errors.append(CheckConfigError(str(message), domain, config))
        return self

    @property
    def error_str(self) -> str:
        """Concatenate all errors to a string."""
        return "\n".join([err.message for err in self.errors])

    def add_warning(
        self,
        message: str,
        domain: str | None = None,
        config: ConfigType | None = None,
    ) -> Self:
        """Add a warning."""
        self.warnings.append(CheckConfigError(str(message), domain, config))
        return self

    @property
    def warning_str(self) -> str:
        """Concatenate all warnings to a string."""
        return "\n".join([err.message for err in self.warnings])


async def async_check_ha_config_file(  # noqa: C901
    hass: HomeAssistant,
) -> HomeAssistantConfig:
    """Load and check if Home Assistant configuration file is valid.

    This method is a coroutine.
    """
    result = HomeAssistantConfig()
    async_clear_install_history(hass)

    def _pack_error(
        hass: HomeAssistant,
        package: str,
        component: str | None,
        config: ConfigType,
        message: str,
    ) -> None:
        """Handle errors from packages."""
        message = f"Setup of package '{package}' failed: {message}"
        domain = f"homeassistant.packages.{package}{'.' + component if component is not None else ''}"
        pack_config = core_config[CONF_PACKAGES].get(package, config)
        result.add_warning(message, domain, pack_config)

    def _comp_error(
        ex: vol.Invalid | HomeAssistantError,
        domain: str,
        component_config: ConfigType,
        config_to_attach: ConfigType,
    ) -> None:
        """Handle errors from components."""
        if isinstance(ex, vol.Invalid):
            message = format_schema_error(hass, ex, domain, component_config)
        else:
            message = format_homeassistant_error(hass, ex, domain, component_config)
        if domain in frontend_dependencies:
            result.add_error(message, domain, config_to_attach)
        else:
            result.add_warning(message, domain, config_to_attach)

    async def _get_integration(
        hass: HomeAssistant, domain: str
    ) -> loader.Integration | None:
        """Get an integration."""
        integration: loader.Integration | None = None
        try:
            integration = await async_get_integration_with_requirements(hass, domain)
        except loader.IntegrationNotFound as ex:
            # We get this error if an integration is not found. In recovery mode and
            # safe mode, this currently happens for all custom integrations. Don't
            # show errors for a missing integration in recovery mode or safe mode to
            # not confuse the user.
            if not hass.config.recovery_mode and not hass.config.safe_mode:
                result.add_warning(f"Integration error: {domain} - {ex}")
        except RequirementsNotFound as ex:
            result.add_warning(f"Integration error: {domain} - {ex}")
        return integration

    # Load configuration.yaml
    config_path = hass.config.path(YAML_CONFIG_FILE)
    try:
        if not await hass.async_add_executor_job(os.path.isfile, config_path):
            return result.add_error("File configuration.yaml not found.")

        config = await hass.async_add_executor_job(
            load_yaml_config_file,
            config_path,
            yaml_loader.Secrets(Path(hass.config.config_dir)),
        )
    except FileNotFoundError:
        return result.add_error(f"File not found: {config_path}")
    except HomeAssistantError as err:
        return result.add_error(f"Error loading {config_path}: {err}")

    # Extract and validate core [homeassistant] config
    core_config = config.pop(HOMEASSISTANT_DOMAIN, {})
    try:
        core_config = CORE_CONFIG_SCHEMA(core_config)
        result[HOMEASSISTANT_DOMAIN] = core_config

        # Merge packages
        await merge_packages_config(
            hass, config, core_config.get(CONF_PACKAGES, {}), _pack_error
        )
    except vol.Invalid as err:
        result.add_error(
            format_schema_error(hass, err, HOMEASSISTANT_DOMAIN, core_config),
            HOMEASSISTANT_DOMAIN,
            core_config,
        )
        core_config = {}
    core_config.pop(CONF_PACKAGES, None)

    # Filter out repeating config sections
    components = {cv.domain_key(key) for key in config}

    frontend_dependencies: set[str] = set()
    if "frontend" in components or "default_config" in components:
        frontend = await _get_integration(hass, "frontend")
        if frontend:
            await frontend.resolve_dependencies()
            frontend_dependencies = frontend.all_dependencies | {"frontend"}

    # Process and validate config
    for domain in components:
        if not (integration := await _get_integration(hass, domain)):
            continue

        try:
            component = await integration.async_get_component()
        except ImportError as ex:
            result.add_warning(f"Component error: {domain} - {ex}")
            continue

        # Check if the integration has a custom config validator
        config_validator = None
        if integration.platforms_exists(("config",)):
            try:
                config_validator = await integration.async_get_platform("config")
            except ImportError as err:
                # Filter out import error of the config platform.
                # If the config platform contains bad imports, make sure
                # that still fails.
                if err.name != f"{integration.pkg_path}.config":
                    result.add_error(f"Error importing config platform {domain}: {err}")
                    continue

        if config_validator is not None and hasattr(
            config_validator, "async_validate_config"
        ):
            try:
                result[domain] = (
                    await config_validator.async_validate_config(hass, config)
                )[domain]
                continue
            except (vol.Invalid, HomeAssistantError) as ex:
                _comp_error(ex, domain, config, config[domain])
                continue
            except Exception as err:
                logging.getLogger(__name__).exception(
                    "Unexpected error validating config"
                )
                result.add_error(
                    f"Unexpected error calling config validator: {err}",
                    domain,
                    config.get(domain),
                )
                continue

        config_schema = getattr(component, "CONFIG_SCHEMA", None)
        if config_schema is not None:
            try:
                validated_config = await cv.async_validate(hass, config_schema, config)
                # Don't fail if the validator removed the domain from the config
                if domain in validated_config:
                    result[domain] = validated_config[domain]
            except vol.Invalid as ex:
                _comp_error(ex, domain, config, config[domain])
                continue

        component_platform_schema = getattr(
            component,
            "PLATFORM_SCHEMA_BASE",
            getattr(component, "PLATFORM_SCHEMA", None),
        )

        if component_platform_schema is None:
            continue

        platforms = []
        for p_name, p_config in config_per_platform(config, domain):
            # Validate component specific platform schema
            try:
                p_validated = await cv.async_validate(
                    hass, component_platform_schema, p_config
                )
            except vol.Invalid as ex:
                _comp_error(ex, domain, p_config, p_config)
                continue

            # Not all platform components follow same pattern for platforms
            # So if p_name is None we are not going to validate platform
            # (the automation component is one of them)
            if p_name is None:
                platforms.append(p_validated)
                continue

            try:
                p_integration = await async_get_integration_with_requirements(
                    hass, p_name
                )
                platform = await p_integration.async_get_platform(domain)
            except loader.IntegrationNotFound as ex:
                # We get this error if an integration is not found. In recovery mode and
                # safe mode, this currently happens for all custom integrations. Don't
                # show errors for a missing integration in recovery mode or safe mode to
                # not confuse the user.
                if not hass.config.recovery_mode and not hass.config.safe_mode:
                    result.add_warning(
                        f"Platform error '{domain}' from integration '{p_name}' - {ex}"
                    )
                continue
            except (
                RequirementsNotFound,
                ImportError,
            ) as ex:
                result.add_warning(
                    f"Platform error '{domain}' from integration '{p_name}' - {ex}"
                )
                continue

            # Validate platform specific schema
            platform_schema = getattr(platform, "PLATFORM_SCHEMA", None)
            if platform_schema is not None:
                try:
                    p_validated = platform_schema(p_validated)
                except vol.Invalid as ex:
                    _comp_error(ex, f"{domain}.{p_name}", p_config, p_config)
                    continue

            platforms.append(p_validated)

        # Remove config for current component and add validated config back in.
        for filter_comp in extract_domain_configs(config, domain):
            del config[filter_comp]
        result[domain] = platforms

    return result
